Plot Twist

Adding Interactivity to the
Elegance of ggplot2 with ggiraph

Tanya Shapiro & Cédric Scherer
useR 2025

Hi, I'm Tanya

Tanya Logo
BEFORE
Data Professional with Insurance Background
NOW
Founder, Consultant IndieVisual

Hi, I'm Cédric

Cedric Logo
BEFORE
Computational Ecologist
NOW
Independent professional in data visualization & information design

Why we ❤️ ggplot2

Slide comparing ggplot2 to a layered cake.

A collection of extension packages for (and built with) ggplot2. A wild mixture of the most popular packages, packages for very specific use cases, packages that provide color palettes, and very experimental stuff.

An illustration by Allison Horst: A person in a cape that reads “code hero” who looks like they are flying through the air while typing on a computer while saying “I’m doing a think all on my own!” The coder’s arms and legs have ropes attached to two hot air balloons lifting them up, with labels on the balloons including “teachers”, “bloggers”, “friends”, “developers”. Below the code hero, several people carry a trampoline with labels “support” and “community” that will catch them if they fall.

Interactive Data Viz

Static plots tell a story

Interactive plots invites people to explore the story

Placeholder

Ced to put bike showcase here

Where To Start?

Goldilocks trying to find the right fit for interactive viz

The ggiraph Philosophy

“If you know ggplot2…you already know ggiraph”

-Plausible quote from Hadley Wickham

Hadley Wickham

Hadley Wickham, Father of ggplot2

Total of 50 interactive ggiraph geoms!

Consistent naming convention to match ggplot2 geoms

ggplot2 ggiraph
geom_point ➡️ geom_point_interactive
geom_text ➡️ geom_text_interactive
geom_line ➡️ geom_line_interactive
geom_tile ➡️ geom_tile_interactive

Export result directly to HTML - or use with Quarto, R Markdown, or Shiny

Examples: Tooltips

Setting up Tooltips

Ced to put in simple code snippit here

Code - Advanced Tooltip

p_simpsons_base <-
  simpsons_imdb |> 
  mutate(
    title_wrapped = stringr::str_replace_all(stringr::str_wrap(title, 22), "\\n", "<br>"),
    text_color = if_else(rating > 6.3 & rating < 8.5, "black", "white"),
    lab = paste0("<span style='font-family:rethink sans;color:", text_color, ";'>", "S", 
                 sprintf("%02d", season), " E", sprintf("%02d", episode), 
                 "<br><b style='font-size:150%;font-weight:600;font-family:piazzolla;'>", 
                 title_wrapped, "</b><br><br>IMDb Rating: ", sprintf("%1.1f", rating))
  )

p_simpsons_advanced <-
  p_simpsons_base +
  geom_tile_interactive(aes(tooltip = lab, data_id = id), color = "white", stroke = .2) +
  geomtextpath::geom_texthline(
    yintercept = 9.5, linewidth = 1, label = "Season 10 starts", 
    vjust = 1.4, hjust = .995, family = "Rethink Sans", lineheight = .6
  )

Example: Hovering

Code - Basic Hover

doctor_who_basic_plot<-ggplot() +
   #interactive points per episode
   ggiraph::geom_jitter_interactive(
     data = df_eps,
     position = position_jitter(seed = 42, height = .2, width =3),
     mapping = aes(
       data_id = story_number,
       x = rating,
       y = reorder(doctor, avg_rating),
       fill = I(color),
       tooltip = tooltip
     ),
     shape = 21,
     color = "black",
     size = 3,
     alpha = 0.8
   ) +
   geomtextpath::geom_textvline(
     mapping = aes(
       xintercept = overall_avg,
       label = paste0("Overall Avg: ", round(overall_avg, 0))
     ),
     size = 3,
     color = pal_line,
     hjust = 0.86,
     vjust = -.2,
     family = "Roboto"
   ) +
   geom_segment(
     data = df_doc_avg,
     mapping = aes(
       x = avg_rating,
       xend = overall_avg,
       y = doctor,
       yend = doctor
     ),
     color = pal_line
   ) +
   geom_point(
     data = df_doc_avg,
     mapping = aes(x = avg_rating, y = doctor, fill = I(color)),
     shape = 21, 
     color = "white",
     size = 10
   ) +
   geom_image(
     data = df_doc_avg,
     mapping = aes(x = avg_rating, y = doctor, image = image),
     size = 0.06,
     asp = 1.61
   ) +
   geom_text(
     data = df_doc_avg,
     mapping = aes(
       x = avg_rating,
       y = doctor,
       label = round(avg_rating, 1)
     ),
     size = 2.5,
     fontface= "bold",
     color = "white",
     vjust = 3.75,
     family = "Roboto"
   ) +
   geom_textbox(
     data = df_doc_avg,
     mapping = aes(x = 59.1, y = doctor, label = label),
     family = "Roboto",
     fill = NA,
     box.size = NA,
     box.padding = unit(rep(0, 4), "pt"),
     color = pal_text,
     hjust = 0
   ) +
   #arrows
   annotate(
     geom = "text",
     label = "Avg Rating\nper Doctor",
     x = 76,
     y = 2.5,
     size = 2.5,
     color = "white",
     family = "Roboto"
   ) +
   geom_curve(
     mapping = aes(
       x = 77,
       xend = 81.4,
       y = 2.7,
       yend = 3
     ),
     color = "white",
     curvature = -0.2,
     linewidth = 0.3,
     arrow = arrow(length = unit(0.08, "in"))
   ) +
   geom_curve(
     mapping = aes(
       x = 77,
       xend = 80.8,
       y = 2.3,
       yend = 2
     ),
     color = "white",
     curvature = 0.2,
     linewidth = 0.3,
     arrow = arrow(length = unit(0.08, "in"))
   ) +
   scale_x_continuous(
     limits = c(59, 95),
     expand = c(0, 0),
     breaks = c(70, 75, 80, 85, 90, 95)
   ) +
   coord_equal(ratio = 50 / 12) +
   labs(
     title = "Doctor Who was The Best?",
     subtitle = "Ratings by Episode and Doctor for the popular TV series, Doctor Who.",
     x = "Rating"
   )+
   theme(
     legend.position = "none",
     plot.background = element_rect(fill = pal_bg, color = pal_bg),
     panel.background = element_blank(),
     panel.grid = element_blank(),
     plot.margin = margin(
       l = 20,
       r = 40,
       b = 10,
       t = 20
     ),
     plot.caption = element_text(size = 7, color = "grey80"),
     plot.title = element_text(
       size = 14,
       face = "bold",
       margin = margin(b = 5)
     ),
     plot.subtitle  = element_text(size = 9, color = "#BABABA"),
     text = element_text(color = pal_text, family = "Roboto"),
     axis.text = element_text(color = pal_text, family = "Roboto Mono"),
     axis.text.y = element_blank(),
     axis.title.y = element_blank(),
     axis.title.x = element_textbox_simple(
       margin = margin(t = 10),
       halign = 0.675,
       hjust = 0.5
     ),
     axis.ticks = element_blank()
   )
 


 ggiraph::girafe(
   ggobj = doctor_who_basic_plot,
   options = list(
      ggiraph::opts_toolbar(saveaspng = F),
      ggiraph::opts_tooltip(css = "font-family:Roboto;"),
      #modify hover css
      ggiraph::opts_hover(css = "fill:white;stroke:grey;cursor:help;")
      )
   )

Code - Advanced Hover

ggiraph::girafe(
   ggobj = doctor_who_advanced_plot,
   width_svg = 6.125, height_svg = 4.5,
   options = list(
     #turnoff download png
    ggiraph::opts_toolbar(saveaspng = F),
    ggiraph::opts_sizing(width = .8),
    #default tooltip font
    ggiraph::opts_tooltip(
      css = "font-family:Roboto;"
    ),
    #remove default opts_hover settings
    ggiraph::opts_hover(css=""),
    #inverted hover, use girafe_css for more control on hover elements
    ggiraph::opts_hover_inv(
      girafe_css(
        css = "", 
        point = "fill:#515151",
        text = NULL
      )
      )
   )
 )

…a creative use case with ggiraph hover

Example: Combo Plots

Combo Plots

Code - Combo Plot

combined_owid <- plot_owid + map_owid +
  plot_layout(ncol = 2, widths = c(.4, .6)) +
  plot_annotation(theme = theme(plot.margin = margin(12, 12, 12, 12)))

girafe(
  ggobj = combined_owid, width_svg = 12, height_svg = 5.3,
  options = list(
    opts_tooltip(use_fill = TRUE, css = "
    font-size: 17px;
    font-weight: 400;
    font-family: Spline Sans;
    color:white;
    padding: 10px;
    border:2px solid white;
    border-radius: 5px;
    "),
    opts_hover(css = "stroke: white; stroke-width: 0.5px; opacity: 1;"),
    opts_hover_inv(css = "opacity: 0.2;"),
    opts_toolbar(position = "bottomright"),
    opts_zoom(min = 1, max = 4)
  )
)

Example: Shiny

Outro

Placeholder for outro

Screenshot of our interactive online course "ggplot2 [un]charted"

ggplot2-uncharted.com